使用 JShell API 实现 Java 源代码浏览器
Sundar Athijegannathan 发表于 2022 年 11 月 21 日除了作为命令行 REPL 工具之外,jshell
还提供了一个编程 API。
从 JDK 19 开始,jshell
会高亮显示 Java 代码段中的关键字、标识符和已弃用 API。这个新特性也通过jshell API 公开。
下面的示例展示了如何使用 JShell API 和 JDK 18 中引入的简单 Web 服务器 API 来实现一个简单的 Java 源代码浏览器。由于它是作为Java shebang 脚本实现的,因此可以直接调用。您只需要传递一个包含一些 Java 源代码的目录。
./srcbrowser ../../jdk/open/src
并在浏览器中打开 https://127.0.0.1:8080 来浏览源代码。
来源
#!/usr/bin/java -source 19
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of Oracle nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.*;
import java.nio.file.*;
import java.net.*;
import java.util.*;
import java.util.stream.*;
import java.util.function.Predicate;
import java.util.spi.ToolProvider;
import jdk.jshell.*;
import com.sun.net.httpserver.*;
import static com.sun.net.httpserver.SimpleFileServer.*;
/**
* Serves the default file system via webserver.
* Decorates .java source files as HTML using JShell APIs.
*
* ./srcbrowser [<base dir>] [<port>]
*/
class srcbrowser {
// base path to serve
private static Path BASE_DIR;
public static void main(String[] args) throws Exception {
BASE_DIR = args.length > 0? Paths.get(args[0]) : Paths.get(".");
BASE_DIR = BASE_DIR.toAbsolutePath();
var fileHandler = SimpleFileServer.createFileHandler(BASE_DIR);
// if it's a java source file, then send HTML generated using jshell
// else use the default (file system) handler
var handler = HttpHandlers.handleOrElse(
srcbrowser::isJavaSource, srcbrowser::sendHtmlForJava, fileHandler);
var output = SimpleFileServer.createOutputFilter(
System.out, OutputLevel.VERBOSE);
var port = args.length > 1? Integer.parseInt(args[1]) : 8080;
var lookback = new InetSocketAddress(port);
var server = HttpServer.create(lookback, 10, "/", handler, output);
System.out.printf("visit https://127.0.0.1:%d/ from your browser..", port);
server.start();
}
// is this a .java source file
private static boolean isJavaSource(Request r) {
return r.getRequestURI().toString().endsWith(".java");
}
private static void sendHtmlForJava(HttpExchange exchange) {
try {
var path = BASE_DIR.resolve(
// get rid of initial '/' from the URI
exchange.getRequestURI().toString().substring(1));
if (!Files.exists(path)) {
exchange.sendResponseHeaders(404, -1);
exchange.close();
} else {
exchange.sendResponseHeaders(200, 0);
var out = new PrintStream(exchange.getResponseBody(), true);
var src = Files.readString(path);
out.println(srcToHTML(src));
exchange.close();
}
} catch (IOException exp) {
exp.printStackTrace();
}
}
private static String srcToHTML(String src) {
var jshell = JShell.builder().executionEngine("local").build();
var srcAnalysis = jshell.sourceCodeAnalysis();
var buf = new StringBuffer();
buf.append("<html><body><pre><code>");
int index = 0;
for (var hl : srcAnalysis.highlights(src)) {
if (index < hl.start()) {
buf.append(htmlEncode(src.substring(index, hl.start())));
}
buf.append(decorate(hl.attributes(),
src.substring(hl.start(), hl.end())));
index = hl.end();
}
buf.append(htmlEncode(src.substring(index)));
buf.append("</pre></code></body></htm>");
return buf.toString();
}
private static String decorate(Set<SourceCodeAnalysis.Attribute> attrs, String content) {
var buf = new StringBuilder();
// start tags
buf.append(
attrs.stream().map(attr -> switch(attr) {
case DECLARATION -> "<b>";
case DEPRECATED -> "<s>";
case KEYWORD -> "<font color=\"red\">";
}).collect(Collectors.joining()));
// content inside
buf.append(htmlEncode(content));
// end tags
buf.append(
attrs.stream().map(attr -> switch(attr) {
case DECLARATION -> "</b>";
case DEPRECATED -> "</s>";
case KEYWORD -> "</font>";
}).collect(Collectors.joining()));
return buf.toString();
}
private static String htmlEncode(String src) {
var buf = new StringBuilder();
for (char c : src.toCharArray()) {
switch (c) {
case '<' -> buf.append("<");
case '>' -> buf.append(">");
case '&' -> buf.append("&");
case '"' -> buf.append(""");
default -> buf.append(c);
}
}
return buf.toString();
}
}